﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NPMonitor.Interfaces.Internal;
using NPMonitor.DataModel.Internal;
using NPMonitor.DataModel;
using System.Threading;
using System.Diagnostics;

namespace NPMonitor.Implementation.Internal
{
    /// <summary>
    /// An implementation of <see cref="IExecutionQueue"/> that uses a basically only one thread to execute jobs on.
    /// This queue supports thread aborting on shutdown, but not specific timeouts per job.
    /// Each executing job uses one thread.
    /// </summary>
    internal class SequencedExecutionQueue : IExecutionQueue
    {
        #region Members

        private LinkedList<JobExecutionContext> workers = new LinkedList<JobExecutionContext>();

        public event EventHandler<JobFinishedExecutingEventArgs> JobFinishedExecuting;

        public byte Id { get; set; }

        public uint ActiveThreads
        {
            get
            {
                return (uint)workers.Count;
            }
        }

        private uint threadCount;

        public uint ThreadCount
        {
            get
            {
                return 1;
            }
            set
            {
                // Sequenced Execution Queue thread account will always be 1
                // throw new NotSupportedException("Sequenced Execution Queue doesn't allow Thread Count changing");
                threadCount = value;
            }
        }

        public bool IsStopping { get; set; }

        #endregion

        #region Constructors

        public SequencedExecutionQueue()
        {
        }

        #endregion

        public bool Enqueue(JobContext jobContext)
        {
            lock (this)
            {
                if (IsStopping || (ActiveThreads >= ThreadCount))
                {
                    return false;
                }

                JobExecutionContext jobExecutionContext = new JobExecutionContext(jobContext, this);
                Thread thread = new Thread(StartMonitoredJob);

                jobExecutionContext.Thread = thread;
                workers.AddLast(jobExecutionContext);
                thread.Start(jobExecutionContext);

                return true;
            }
        }

        static void StartMonitoredJob(Object jobExecutionContextObject)
        {
            JobExecutionContext jobExecutionContext = (JobExecutionContext)jobExecutionContextObject;
            JobContext jobContext = jobExecutionContext.JobContext;

            // using default timeout if absolute timeout is null or lower than 1 second
            TimeSpan? jobTimeout = jobContext.JobData.AbsoluteTimeout;
            if (!jobTimeout.HasValue || jobTimeout.Value < TimeSpan.FromSeconds(1))
            {
                if (Configuration.ConfigurationSettings.SettingsProvider.DefaultTimeout.HasValue)
                {
                    jobTimeout = Configuration.ConfigurationSettings.SettingsProvider.DefaultTimeout;
                }
            }

            Thread thread = new Thread(ExecuteJob);
            thread.Start(jobExecutionContext);

            jobExecutionContext.Thread = thread; //replace the ref to the thread. This is the actual one we want to be able to stop.

            if (jobTimeout.HasValue && jobTimeout.Value > TimeSpan.FromSeconds(1))
            {
                bool completedWithoutTimeout = thread.Join(jobTimeout.Value);
                if (!completedWithoutTimeout)
                {
                    string message = string.Format("Job has exceeded it's AbsoluteTimeout value of {0} second(s) and was terminated abnormally.",
                        jobTimeout.Value.TotalSeconds);

                    jobContext.JobData.LastErrorMessage = message;
                    jobContext.JobData.Status = JobStatus.ExecutionTimeout;

                    jobExecutionContext.Thread.IsBackground = true;
                    jobExecutionContext.Thread.Abort("TimeOut");

                    int counter = 0;
                    while (jobExecutionContext.Thread.IsAlive)
                    {
                        if (++counter < 5)
                        {
                            Thread.Sleep(1000); // Sleep for 1 second, for quit web driver if exceeded 5 seconds
                        }
                        else
                        {
                            // try to kill web driver by force, (mostly web driver cause thread hold)
                            Utility.WebDriverHelper.ForceQuitWebDriver(Configuration.ConfigurationSettings.SettingsProvider.BrowserType);
                        }
                    }

                    SequencedExecutionQueue queue = (SequencedExecutionQueue)jobExecutionContext.ExecutionQueue;

                    lock (queue.workers)
                    {
                        queue.workers.Remove(jobExecutionContext);
                    }

                    var jobFinishedEvent = queue.JobFinishedExecuting;
                    if (jobFinishedEvent != null)
                    {
                        jobFinishedEvent(queue, new JobFinishedExecutingEventArgs(jobContext.JobData));
                    }
                }
            }
            else
            {
                thread.Join();
            }
        }

        static void ExecuteJob(Object jobExecutionContextObject)
        {
            JobExecutionContext jobExecutionContext = (JobExecutionContext)jobExecutionContextObject;
            JobContext jobContext = jobExecutionContext.JobContext;

            Debug.WriteLine(Helpers.Utils.CurrentServerUtcTime + " : " + jobContext.JobData.Id + " start thread. Queue = " + jobExecutionContext.ExecutionQueue.Id);

            IJobExecutorFactory jobExecutorFactory = new JobExecutorFactory();
            IJobExecutor jobExecutor = jobExecutorFactory.GetJobExecutor(jobContext);
            jobExecutor.ExecuteJob(jobContext);

            SequencedExecutionQueue queue = (SequencedExecutionQueue)jobExecutionContext.ExecutionQueue;
            lock (queue.workers)
            {
                queue.workers.Remove(jobExecutionContext);
            }

            var jobFinishedEvent = queue.JobFinishedExecuting;
            if (jobFinishedEvent != null)
            {
                jobFinishedEvent(queue, new JobFinishedExecutingEventArgs(jobContext.JobData));
            }

            Debug.WriteLine(Helpers.Utils.CurrentServerUtcTime + " : " + jobContext.JobData.Id + " end thread. Queue = " + jobExecutionContext.ExecutionQueue.Id);
        }

        public bool ShutdownRunningJobs()
        {
            lock (workers)
            {
                foreach (var worker in workers)
                {
                    worker.Thread.Abort();
                    while (worker.Thread.IsAlive)
                    {
                        Thread.Sleep(1);
                    }
                }
                workers.Clear();
            }
            return true;
        }
    }
}
